篇首语:本文由编程笔记#小编为大家整理,主要介绍了数据库篇复习篇相关的知识,希望对你有一定的参考价值。
未提交读
读到其它事务未提交的数据(最新的版本)
错误现象:有脏读、不可重复读、幻读现象
脏读现象
tx1 | tx2 |
---|---|
set session transaction isolation level read uncommitted; | |
start transaction; | |
select * from account; /两个账户都为 1000/ | |
start transaction; | |
update account set balance = 2000 where accountNo=1; | |
select * from account; /1号账户2000, 2号账户1000/ |
提交读(RC)
读到其它事务已提交的数据(最新已提交的版本)
错误现象:有不可重复读、幻读现象
使用场景:希望看到最新的有效值
不可重复度现象
tx1 | tx2 |
---|---|
set session transaction isolation level read committed; | |
start transaction; | |
select * from account; /两个账户都为 1000/ | |
update account set balance = 2000 where accountNo=1; | |
select * from account; /1号账户2000, 2号账户1000/ |
可重复读(RR)
在事务范围内,多次读能够保证一致性(快照建立时最新已提交版本)
错误现象:有幻读现象,可以用加锁避免
使用场景:事务内要求更强的一致性,但看到的未必是最新的有效值
幻读现象
tx1 | tx2 |
---|---|
set session transaction isolation level repeatable read; | |
start transaction; | |
select * from account; /存在 1,2 两个账户/ | |
insert into account values(3, 1000); | |
select * from account; /发现还是只有 1,2 两个账户/ | |
insert into account values(3, 5000); /* ERROR 1062 (23000): Duplicate entry ‘3’ for key ‘PRIMARY’ */ |
加锁避免幻读
tx1 | tx2 |
---|---|
set session transaction isolation level repeatable read; | |
start transaction; | |
select * from account; /存在 1,2 两个账户/ | |
select * from account where accountNo=3 for update; | |
insert into account values(3, 1000); /* 阻塞 */ | |
insert into account values(3, 5000); |
串行读
在事务范围内,仅有读读可以并发,读写或写写会阻塞其它事务,用这种办法保证更强的一致性
错误现象:无
串行读避免幻读
tx1 | tx2 |
---|---|
set session transaction isolation level serializable; | |
start transaction; | |
select * from account; /* 存在 1,2 两个账户 */ | |
insert into account values(3, 1000); /* 阻塞 */ | |
insert into account values(3, 5000); |
当前读
即读取最新提交的数据
当前读本质上是基于锁的并发读操作
快照读
读取某一个快照建立时(可以理解为某一时间点)的数据,也称为一致性读。快照读主要体现在 select 时,而不同隔离级别下,select 的行为不同
在 Serializable 隔离级别下 - 普通 select 也变成当前读,即加共享读锁
在 RC 隔离级别下 - 每次 select 都会建立新的快照
在 RR 隔离级别下
快照读本质上读取的是历史数据(原理是回滚段),属于无锁查询
RR 下,快照建立时机 - 第一次 select 时
tx1 | tx2 |
---|---|
set session transaction isolation level repeatable read; | |
start transaction; | |
select * from account; /* 此时建立快照,两个账户为 1000 */ | |
update account set balance = 2000 where accountNo=1; | |
select * from account; /* 两个账户仍为 1000 */ |
如果 tx2 的 update 先执行
tx1 | tx2 |
---|---|
set session transaction isolation level repeatable read; | |
start transaction; | |
update account set balance = 2000 where accountNo=1; | |
select * from account; /* 此时建立快照,1号余额已经为2000 */ |
RR 下,快照建立时机 - 事务启动时
如果希望事务启动时就建立快照,可以添加 with consistent snapshot 选项
tx1 | tx2 |
---|---|
set session transaction isolation level repeatable read; | |
start transaction with consistent snapshot; /* 此时建立快照,两个账户为 1000 */ | |
update account set balance = 2000 where accountNo=1; | |
select * from account; /* 两个账户仍为 1000 */ |
RR 下,快照建立时机 - 修改数据时
tx1 | tx2 |
---|---|
set session transaction isolation level repeatable read; | |
start transaction; | |
select * from account; /* 此时建立快照,两个账户为 1000 */ | |
update account set balance=balance+1000 where accountNo=1; | |
update account set balance=balance+1000 where accountNo=1; | |
select * from account; /* 1号余额为3000 */ |
InnoDB
索引分为聚簇索引与二级索引
支持事务
支持行锁、间隙锁
支持外键
MyISAM
索引只有一种
不支持事务,没有 undo log 和 redo log
仅支持表锁
不支持外键
会保存表的总行数
InnoDB 索引特点
聚簇索引:主键值作为索引数据,叶子节点还包含了所有字段数据,索引和数据是存储在一起的
二级索引:除主键外的其它字段建立的索引称为二级索引。被索引的字段值作为索引数据,叶子节点还包含了主键值
select empno, ename, sal from emp where sal = 800
,这时候可以利用二级索引定位到 800 这个工资,同时还能知道主键值 7369MyISAM 索引特点
被索引字段值作为索引数据,叶子节点还包含了该记录数据页地址,数据和索引是分开存储的
常见索引
哈希索引
平衡二叉树索引
BTree 索引
B+ 树索引
树高计算公式
l
o
g
10
(
N
)
/
l
o
g
10
(
M
)
log_10(N) / log_10(M)
log10(N)/log10(M) 其中 N 为数据行数,M 为分叉数
BTree vs B+Tree
注:这两张图都是仅画了 key,未画 value
B+Tree 新增 key
假设阶数(m)为5
若为空树,那么直接创建一个节点,插入 key 即可,此时这个叶子结点也是根结点。例如,插入 5
插入时,若当前结点 key 的个数小于阶数,则插入结束
依次插入 8、10、15,按 key 大小升序
插入 16,这时到达了阶数限制,所以要进行分裂
叶子节点分裂规则:将这个叶子结点分裂成左右两个叶子结点,左叶子结点包含前 m/2 个(2个)记录,右结点包含剩下的记录,将中间的 key 进位到父结点中。注意:中间的 key 仍会保留在叶子节点一份
插入 17
插入 18,这时当前结点的 key 个数到达 5,进行分裂
分裂成两个结点,左结点 2 个记录,右结点 3 个记录,key 16 进位到父结点中
插入 19、20、21、22、6、9
插入 7,当前结点的 key 个数到达 5,需要分裂
分裂后 key 7 进入到父结点中,这时父节点 key 个数也到达 5
非叶子节点分裂规则:左子结点包含前 (m-1)/2 个 key,将中间的 key 进位到父结点中(不保留),右子节点包含剩余的 key
B+Tree 查询 key
以查询 15 为例
第一次 I/O
第二次 I/O
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-j5wRJyQm-1659863800788)(img/image-20210901175738876-16304902605912.png)]
第三次 I/O
B+Tree 删除叶子节点 key
初始状态
删完有富余。即删除后结点的key的个数 > m/2 – 1,删除操作结束,例如删除 22
删完没富余,但兄弟节点有富余。即兄弟结点 key 有富余( > m/2 – 1 ),向兄弟结点借一个记录,同时替换父节点,例如删除 15
兄弟节点也不富余,合并兄弟叶子节点。即兄弟节点合并成一个新的叶子结点,并删除父结点中的key,将当前结点指向父结点,例如删除 7
也需要删除非叶子节点中的 7,并替换父节点保证区间仍有效
左右兄弟都不够借,合并
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VLOf8HNd-1659863800793)(img/image-20210901180446827.png)]
B+Tree 删除非叶子节点 key
接着上面的操作
非叶子节点 key 的个数 > m/2 – 1,则删除操作结束,否则执行 2
若兄弟结点有富余,父结点 key 下移,兄弟结点 key 上移,删除结束,否则执行 3
若兄弟节点没富余,当前结点和兄弟结点及父结点合并成一个新的结点。重复 1
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rIJW9sM7-1659863800794)(img/image-20210901180511685.png)]
准备数据
修改 MySQL 配置文件,在 [mysqld] 下添加 secure_file_priv= 重启 MySQL 服务器,让选项生效
执行 db.sql 内的脚本,建表
执行
LOAD DATA INFILE 'D:\\\\big_person.txt' INTO TABLE big_person;
注意实际路径根据情况修改
- 测试表 big_person(此表数据量较大,如果与其它表数据一起提供不好管理,故单独提供),数据行数 100 万条,列个数 15 列。为了更快速导入数据,这里采用了 load data infile 命令配合 *.txt 格式数据
索引用于排序
/* 测试单列索引并不能在多列排序时加速 */
create index first_idx on big_person(first_name);
create index last_idx on big_person(last_name);
explain select * from big_person order by last_name, first_name limit 10;
/* 多列排序需要用组合索引 */
alter table big_person drop index first_idx;
alter table big_person drop index last_idx;
create index last_first_idx on big_person(last_name,first_name);
/* 多列排序需要遵循最左前缀原则, 第1个查询可以利用索引,第2,3查询不能利用索引 */
explain select * from big_person order by last_name, first_name limit 10;
explain select * from big_person order by first_name, last_name limit 10;
explain select * from big_person order by first_name limit 10;
/* 多列排序升降序需要一致,查询1可以利用索引,查询2不能利用索引*/
explain select * from big_person order by last_name desc, first_name desc limit 10;
explain select * from big_person order by last_name desc, first_name asc limit 10;
最左前缀原则
若建立组合索引 (a,b,c),则可以利用到索引的排序条件是:
- order by a
- order by a, b
- order by a, b, c
索引用于 where 筛选
/* 模糊查询需要遵循字符串最左前缀原则,查询2可以利用索引,查询1,3不能利用索引 */
explain SELECT * FROM big_person WHERE first_name LIKE 'dav%' LIMIT 5;
explain SELECT * FROM big_person WHERE last_name LIKE 'dav%' LIMIT 5;
explain SELECT * FROM big_person WHERE last_name LIKE '%dav' LIMIT 5;
/* 组合索引需要遵循最左前缀原则,查询1,2可以利用索引,查询3,4不能利用索引 */
create index province_city_county_idx on big_person(province,city,county);
explain SELECT * FROM big_person WHERE province = '上海' AND city='宜兰县' AND county='中西区';
explain SELECT * FROM big_person WHERE county='中西区' AND city='宜兰县' AND province = '上海';
explain SELECT * FROM big_person WHERE city='宜兰县' AND county='中西区';
explain SELECT * FROM big_person WHERE county='中西区';
/* 函数及计算问题,一旦在字段上应用了计算或函数,都会造成索引失效。查询2可以利用索引,查询1不能利用索引 */
create index birthday_idx on big_person(birthday);
explain SELECT * FROM big_person WHERE ADDDATE(birthday,1)='2005-02-10';
explain SELECT * FROM big_person WHERE birthday=ADDDATE('2005-02-10',-1);
/* 隐式类型转换问题
* 查询1会发生隐式类型转换等价于在phone上应用了函数,造成索引失效
* 查询2字段与值类型相同不会类型转换,可以利用索引
*/
create index phone_idx on big_person(phone);
explain SELECT * FROM big_person WHERE phone = 13000013934;
explain SELECT * FROM big_person WHERE phone = '13000013934';
最左前缀原则(leftmost prefix)
若建立组合索引 (a,b,c),则可以利用到索引的查询条件是:
- where a = ?
- where a = ? and b = ? (注意与条件的先后次序无关,也可以是 where b = ? and a = ?,只要出现即可)
- where a = ? and b = ? and c = ? (注意事项同上)
不能利用的例子:
- where b = ?
- where b = ? and c = ?
- where c = ?
特殊情况:
- where a = ? and c = ?(a = ? 会利用索引,但 c = ? 不能利用索引加速,会触发索引条件下推)
索引条件下推
/* 查询 1,2,3,4 都能利用索引,但 4 相当于部分利用了索引,会触发索引条件下推 */
explain SELECT * FROM big_person WHERE province = '上海';
explain SELECT * FROM big_person WHERE province = '上海' AND city='嘉兴市';
explain SELECT * FROM big_person WHERE province = '上海' AND city='嘉兴市' AND county='中西区';
explain SELECT * FROM big_person WHERE province = '上海' AND county='中西区';
索引条件下推
- MySQL 执行条件判断的时机有两处:
- 服务层(上层,不包括索引实现)
- 引擎层(下层,包括了索引实现,可以利用)
- 上面查询 4 中有 province 条件能够利用索引,在引擎层执行,但 county 条件仍然要交给服务层处理
- 在 5.6 之前,服务层需要判断所有记录的 county 条件,性能非常低
- 5.6 以后,引擎层会先根据 province 条件过滤,满足条件的记录才在服务层处理 county 条件
我们现在用的是 5.6 以上版本,所以没有体会,可以用下面的语句关闭索引下推优化,再测试一下性能
SET optimizer_switch = 'index_condition_pushdown=off';
SELECT * FROM big_person WHERE province = '上海' AND county='中西区';
二级索引覆盖
explain SELECT * FROM big_person WHERE province = '上海' AND city='宜兰县' AND county= '中西区';
explain SELECT id,province,city,county FROM big_person WHERE province = '上海' AND city='宜兰县' AND county='中西区';
根据查询条件查询 1,2 都会先走二级索引,但是二级索引仅包含了 (province, city, county) 和 id 信息
其它注意事项
例如:
create index first_idx on big_person(first_name);
/* 不会利用索引,因为优化器发现查询记录数太多,还不如直接全表扫描 */
explain SELECT * FROM big_person WHERE first_name > 'Jenni';
/* 会利用索引,因为优化器发现查询记录数不太多 */
explain SELECT * FROM big_person WHERE first_name > 'Willia';
/* 同一字段的不同值利用 or 连接,会利用索引 */
explain select * from big_person where id = 1 or id = 190839;
/* 不同字段利用 or 连接,会利用索引(底层分别用了两个索引) */
explain select * from big_person where first_name = 'David' or last_name = 'Thomas';
/* in 会利用索引 */
explain select * from big_person where first_name in ('Mark', 'Kevin','David');
/* not in 不会利用索引的情况 */
explain select * from big_person where first_name not in ('Mark', 'Kevin','David');
/* not in 会利用索引的情况 */
explain select id from big_person where first_name not in ('Mark', 'Kevin','David');
执行 SQL 语句 select * from user where id = 1 时发生了什么
连接器:负责建立连接、检查权限、连接超时时间由 wait_timeout 控制,默认 8 小时
查询缓存:会将 SQL 和查询结果以键值对方式进行缓存,修改操作会以表单位导致缓存失效
分析器:词法、语法分析
优化器:决定用哪个索引,决定表的连接顺序等
执行器:根据存储引擎类型,调用存储引擎接口
存储引擎:数据的读写接口,索引、表都在此层实现
undo log
redo log
redo log 的作用主要是实现 ACID 中的持久性,保证提交的数据不丢失
它由两部分组成,内存中的 redo log buffer,磁盘上的 redo log file
全局锁
用作全量备份时,保证表与表之间的数据一致性
如果不加任何包含,数据备份时就可能产生不一致的情况,如下图所示
全局锁的语法:
flush tables with read lock;
注意
但 flush tables 属于比较重的操作,可以使用 --single-transaction 参数来完成不加锁的一致性备份(仅针对 InnoDB 引擎的表)
mysqldump --single-transaction -uroot -p test > 1.sql
表级锁 - 表锁
表级锁 - 元数据锁
即 metadata-lock(MDL),主要是为了避免 DML 与 DDL 冲突,DML 的元数据锁之间不互斥
加元数据锁的几种情况
lock tables read/write
,类型为 SHARED_READ_ONLY 和 SHARED_NO_READ_WRITEalter table
,类型为 EXCLUSIVE,与其它 MDL 都互斥select,select … lock in share mode
,类型为 SHARED_READinsert,update,delete,select for update
,类型为 SHARED_WRITE查看元数据锁(适用于 MySQL 8.0 以上版本)
select object_type,object_schema,object_name,lock_type,lock_duration from performance_schema.metadata_locks;
表级锁 - IS(意向共享) 与 IX(意向排他)
select … lock in share mode
会加 IS 锁insert,update,delete, select … for update
会加 IX 锁select object_schema,object_name,index_name,lock_type,lock_mode,lock_data from performance_schema.data_locks;
行级锁
种类
查看行级锁
select object_schema,object_name,index_name,lock_type,lock_mode,lock_data from performance_schema.data_locks where object_name='表名';
注意
- 它们锁定的其实都是索引上的行与间隙,根据索引的有序性来确定间隙
测试数据
create table t (id int primary key, name varchar(10),age int, key (name));
insert into t values(1, 'zhangsan',18);
insert into t values(2, 'lisi',20);
insert into t values(3, 'wangwu',21);
insert into t values(4, 'zhangsan', 17);
insert into t values(8,'zhang',18);
insert into t values(12,'zhang',20);
说明
- 1,2,3,4 之间其实并不可能有间隙
- 4 与 8 之间有间隙
- 8 与 12 之间有间隙
- 12 与正无穷大之间有间隙
- 其实我们的例子中还有负无穷大与 1 之间的间隙,想避免负数可以通过建表时选择数据类型为 unsigned int
间隙锁例子
事务1:
begin;
select * from t where id = 9 for update; /* 锁住的是 8 与 12 之间的间隙 */
事务2:
update t set age=100 where id = 8; /* 不会阻塞 */
update t set age=100 where id = 12; /* 不会阻塞 */
insert into t values(10,'aaa',18); /* 会阻塞 */
临键锁和记录锁例子
事务1:
begin;
select * from t where id >= 8 for update;
事务2:
insert into t values(7,'aaa',18); /* 不会阻塞 */
update t set age=100 where id = 8; /* 会阻塞 */
insert into t values(